@@ -1,5 +1,6 @@ |
||
| 1 | 1 |
# Changes |
| 2 | 2 |
|
| 3 |
+* 0.31 (Jan 2, 2014) - Agents now have an optional keep\_events\_for option that is propagated to created events' expires\_at field, and they update their events' expires\_at fields on change. |
|
| 3 | 4 |
* 0.3 (Jan 1, 2014) - Remove symbolization of memory, options, and payloads; convert memory, options, and payloads to JSON from YAML. Migration will perform conversion and adjust tables to be UTF-8. Recommend making a DB backup before migrating. |
| 4 | 5 |
* 0.2 (Nov 6, 2013) - PeakDetectorAgent now uses `window_duration_in_days` and `min_peak_spacing_in_days`. Additionally, peaks trigger when the time series rises over the standard deviation multiple, not after it starts to fall. |
| 5 | 6 |
* June 29, 2013 - Removed rails\_admin because it was causing deployment issues. Better to have people install their favorite admin tool if they want one. |
@@ -34,6 +34,12 @@ showLinks = -> |
||
| 34 | 34 |
$(".link-region .cannot-receive-events").hide()
|
| 35 | 35 |
showEventDescriptions() |
| 36 | 36 |
|
| 37 |
+hideEventCreation = -> |
|
| 38 |
+ $(".event-related-region").hide()
|
|
| 39 |
+ |
|
| 40 |
+showEventCreation = -> |
|
| 41 |
+ $(".event-related-region").show()
|
|
| 42 |
+ |
|
| 37 | 43 |
showEventDescriptions = -> |
| 38 | 44 |
if $("#agent_source_ids").val()
|
| 39 | 45 |
$.getJSON "/agents/event_descriptions", { ids: $("#agent_source_ids").val().join(",") }, (json) =>
|
@@ -132,6 +138,11 @@ $(document).ready -> |
||
| 132 | 138 |
else |
| 133 | 139 |
hideLinks() |
| 134 | 140 |
|
| 141 |
+ if json.can_create_events |
|
| 142 |
+ showEventCreation() |
|
| 143 |
+ else |
|
| 144 |
+ hideEventCreation() |
|
| 145 |
+ |
|
| 135 | 146 |
$(".description").html(json.description_html) if json.description_html?
|
| 136 | 147 |
|
| 137 | 148 |
if $("#agent_options").hasClass("showing-default") || $("#agent_options").val().match(/\A\s*(\{\s*\}|)\s*\Z/g)
|
@@ -153,3 +164,9 @@ $(document).ready -> |
||
| 153 | 164 |
showLinks() |
| 154 | 165 |
else |
| 155 | 166 |
hideLinks() |
| 167 |
+ |
|
| 168 |
+ if $(".event-related-region")
|
|
| 169 |
+ if $(".event-related-region").data("can-create-events") == true
|
|
| 170 |
+ showEventCreation() |
|
| 171 |
+ else |
|
| 172 |
+ hideEventCreation() |
@@ -33,6 +33,7 @@ class AgentsController < ApplicationController |
||
| 33 | 33 |
render :json => {
|
| 34 | 34 |
:can_be_scheduled => agent.can_be_scheduled?, |
| 35 | 35 |
:can_receive_events => agent.can_receive_events?, |
| 36 |
+ :can_create_events => agent.can_create_events?, |
|
| 36 | 37 |
:options => agent.default_options, |
| 37 | 38 |
:description_html => agent.html_description |
| 38 | 39 |
} |
@@ -15,11 +15,14 @@ class Agent < ActiveRecord::Base |
||
| 15 | 15 |
SCHEDULES = %w[every_2m every_5m every_10m every_30m every_1h every_2h every_5h every_12h every_1d every_2d every_7d |
| 16 | 16 |
midnight 1am 2am 3am 4am 5am 6am 7am 8am 9am 10am 11am noon 1pm 2pm 3pm 4pm 5pm 6pm 7pm 8pm 9pm 10pm 11pm] |
| 17 | 17 |
|
| 18 |
- attr_accessible :options, :memory, :name, :type, :schedule, :source_ids |
|
| 18 |
+ EVENT_RETENTION_SCHEDULES = [["Forever", 0], ["1 day", 1], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n] })]
|
|
| 19 |
+ |
|
| 20 |
+ attr_accessible :options, :memory, :name, :type, :schedule, :source_ids, :keep_events_for |
|
| 19 | 21 |
|
| 20 | 22 |
json_serialize :options, :memory |
| 21 | 23 |
|
| 22 | 24 |
validates_presence_of :name, :user |
| 25 |
+ validates_inclusion_of :keep_events_for, :in => EVENT_RETENTION_SCHEDULES.map(&:last) |
|
| 23 | 26 |
validate :sources_are_owned |
| 24 | 27 |
validate :validate_schedule |
| 25 | 28 |
validate :validate_options |
@@ -29,6 +32,7 @@ class Agent < ActiveRecord::Base |
||
| 29 | 32 |
before_validation :unschedule_if_cannot_schedule |
| 30 | 33 |
before_save :unschedule_if_cannot_schedule |
| 31 | 34 |
before_create :set_last_checked_event_id |
| 35 |
+ after_save :possibly_update_event_expirations |
|
| 32 | 36 |
|
| 33 | 37 |
belongs_to :user, :inverse_of => :agents |
| 34 | 38 |
has_many :events, :dependent => :delete_all, :inverse_of => :agent, :order => "events.id desc" |
@@ -87,21 +91,23 @@ class Agent < ActiveRecord::Base |
||
| 87 | 91 |
last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes) |
| 88 | 92 |
end |
| 89 | 93 |
|
| 90 |
- def sources_are_owned |
|
| 91 |
- errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user }
|
|
| 92 |
- end |
|
| 93 |
- |
|
| 94 | 94 |
def create_event(attrs) |
| 95 | 95 |
if can_create_events? |
| 96 |
- events.create!({ :user => user }.merge(attrs))
|
|
| 96 |
+ events.create!({ :user => user, :expires_at => new_event_expiration_date }.merge(attrs))
|
|
| 97 | 97 |
else |
| 98 | 98 |
error "This Agent cannot create events!" |
| 99 | 99 |
end |
| 100 | 100 |
end |
| 101 | 101 |
|
| 102 |
- def validate_schedule |
|
| 103 |
- unless cannot_be_scheduled? |
|
| 104 |
- errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s) |
|
| 102 |
+ def new_event_expiration_date |
|
| 103 |
+ keep_events_for > 0 ? keep_events_for.days.from_now : nil |
|
| 104 |
+ end |
|
| 105 |
+ |
|
| 106 |
+ def update_event_expirations! |
|
| 107 |
+ if keep_events_for == 0 |
|
| 108 |
+ events.update_all :expires_at => nil |
|
| 109 |
+ else |
|
| 110 |
+ events.update_all "expires_at = DATE_ADD(`created_at`, INTERVAL #{keep_events_for.to_i} DAY)"
|
|
| 105 | 111 |
end |
| 106 | 112 |
end |
| 107 | 113 |
|
@@ -116,14 +122,6 @@ class Agent < ActiveRecord::Base |
||
| 116 | 122 |
end |
| 117 | 123 |
end |
| 118 | 124 |
|
| 119 |
- def set_default_schedule |
|
| 120 |
- self.schedule = default_schedule unless schedule.present? || cannot_be_scheduled? |
|
| 121 |
- end |
|
| 122 |
- |
|
| 123 |
- def unschedule_if_cannot_schedule |
|
| 124 |
- self.schedule = nil if cannot_be_scheduled? |
|
| 125 |
- end |
|
| 126 |
- |
|
| 127 | 125 |
def default_schedule |
| 128 | 126 |
self.class.default_schedule |
| 129 | 127 |
end |
@@ -152,10 +150,13 @@ class Agent < ActiveRecord::Base |
||
| 152 | 150 |
!cannot_create_events? |
| 153 | 151 |
end |
| 154 | 152 |
|
| 155 |
- def set_last_checked_event_id |
|
| 156 |
- if newest_event_id = Event.order("id desc").limit(1).pluck(:id).first
|
|
| 157 |
- self.last_checked_event_id = newest_event_id |
|
| 158 |
- end |
|
| 153 |
+ def log(message, options = {})
|
|
| 154 |
+ puts "Agent##{id}: #{message}" unless Rails.env.test?
|
|
| 155 |
+ AgentLog.log_for_agent(self, message, options) |
|
| 156 |
+ end |
|
| 157 |
+ |
|
| 158 |
+ def error(message, options = {})
|
|
| 159 |
+ log(message, options.merge(:level => 4)) |
|
| 159 | 160 |
end |
| 160 | 161 |
|
| 161 | 162 |
def delete_logs! |
@@ -163,16 +164,38 @@ class Agent < ActiveRecord::Base |
||
| 163 | 164 |
update_column :last_error_log_at, nil |
| 164 | 165 |
end |
| 165 | 166 |
|
| 166 |
- def log(message, options = {})
|
|
| 167 |
- puts "Agent##{id}: #{message}" unless Rails.env.test?
|
|
| 168 |
- AgentLog.log_for_agent(self, message, options) |
|
| 167 |
+ # Validations and Callbacks |
|
| 168 |
+ |
|
| 169 |
+ def sources_are_owned |
|
| 170 |
+ errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user }
|
|
| 169 | 171 |
end |
| 170 | 172 |
|
| 171 |
- def error(message, options = {})
|
|
| 172 |
- log(message, options.merge(:level => 4)) |
|
| 173 |
+ def validate_schedule |
|
| 174 |
+ unless cannot_be_scheduled? |
|
| 175 |
+ errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s) |
|
| 176 |
+ end |
|
| 177 |
+ end |
|
| 178 |
+ |
|
| 179 |
+ def set_default_schedule |
|
| 180 |
+ self.schedule = default_schedule unless schedule.present? || cannot_be_scheduled? |
|
| 181 |
+ end |
|
| 182 |
+ |
|
| 183 |
+ def unschedule_if_cannot_schedule |
|
| 184 |
+ self.schedule = nil if cannot_be_scheduled? |
|
| 185 |
+ end |
|
| 186 |
+ |
|
| 187 |
+ def set_last_checked_event_id |
|
| 188 |
+ if newest_event_id = Event.order("id desc").limit(1).pluck(:id).first
|
|
| 189 |
+ self.last_checked_event_id = newest_event_id |
|
| 190 |
+ end |
|
| 191 |
+ end |
|
| 192 |
+ |
|
| 193 |
+ def possibly_update_event_expirations |
|
| 194 |
+ update_event_expirations! if keep_events_for_changed? |
|
| 173 | 195 |
end |
| 174 | 196 |
|
| 175 | 197 |
# Class Methods |
| 198 |
+ |
|
| 176 | 199 |
class << self |
| 177 | 200 |
def cannot_be_scheduled! |
| 178 | 201 |
@cannot_be_scheduled = true |
@@ -21,6 +21,8 @@ class Event < ActiveRecord::Base |
||
| 21 | 21 |
end |
| 22 | 22 |
|
| 23 | 23 |
def self.cleanup_expired! |
| 24 |
+ affected_agents = Event.where("expires_at IS NOT NULL AND expires_at < ?", Time.now).group("agent_id").pluck(:agent_id)
|
|
| 24 | 25 |
Event.where("expires_at IS NOT NULL AND expires_at < ?", Time.now).delete_all
|
| 26 |
+ Agent.where(:id => affected_agents).update_all "events_count = (select count(*) from events where agent_id = agents.id)" |
|
| 25 | 27 |
end |
| 26 | 28 |
end |
@@ -44,6 +44,16 @@ |
||
| 44 | 44 |
</div> |
| 45 | 45 |
</div> |
| 46 | 46 |
|
| 47 |
+ |
|
| 48 |
+ <div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>"> |
|
| 49 |
+ <div class="control-group"> |
|
| 50 |
+ <%= f.label :keep_events_for, "Keep events", :class => 'control-label' %> |
|
| 51 |
+ <div class="controls"> |
|
| 52 |
+ <%= f.select :keep_events_for, options_for_select(Agent::EVENT_RETENTION_SCHEDULES, @agent.keep_events_for), {}, :class => 'span4' %>
|
|
| 53 |
+ </div> |
|
| 54 |
+ </div> |
|
| 55 |
+ </div> |
|
| 56 |
+ |
|
| 47 | 57 |
<div class="control-group"> |
| 48 | 58 |
<%= f.label :sources, :class => 'control-label' %> |
| 49 | 59 |
<div class="controls link-region" data-can-receive-events="<%= @agent.can_receive_events? %>"> |
@@ -84,6 +84,11 @@ |
||
| 84 | 84 |
|
| 85 | 85 |
<% if @agent.can_create_events? %> |
| 86 | 86 |
<p> |
| 87 |
+ <b>Keep events:</b> |
|
| 88 |
+ <%= (Agent::EVENT_RETENTION_SCHEDULES.detect {|s| s.last == @agent.keep_events_for } || [@agent.keep_events_for]).first %>
|
|
| 89 |
+ </p> |
|
| 90 |
+ |
|
| 91 |
+ <p> |
|
| 87 | 92 |
<b>Last event created:</b> |
| 88 | 93 |
<%= @agent.last_event_at ? time_ago_in_words(@agent.last_event_at) + " ago" : "never" %> |
| 89 | 94 |
</p> |
@@ -6,6 +6,11 @@ |
||
| 6 | 6 |
</div> |
| 7 | 7 |
|
| 8 | 8 |
<p> |
| 9 |
+ <b>Expires in:</b> |
|
| 10 |
+ <%= @event.expires_at ? time_ago_in_words(@event.expires_at) : 'never' %> |
|
| 11 |
+ </p> |
|
| 12 |
+ |
|
| 13 |
+ <p> |
|
| 9 | 14 |
<b>Payload:</b> |
| 10 | 15 |
<pre><%= Utils.pretty_jsonify @event.payload || {} %></pre>
|
| 11 | 16 |
</p> |
@@ -2,9 +2,10 @@ |
||
| 2 | 2 |
<div class='row'> |
| 3 | 3 |
<div class="span5 offset2"> |
| 4 | 4 |
<h1>Your agents are standing by</h1> |
| 5 |
- <p>Know the world around you</p> |
|
| 5 |
+ <p>Huginn monitors the world and acts on your behalf.</p> |
|
| 6 | 6 |
|
| 7 |
- <%= link_to "Signup", new_user_registration_path, :class => "btn btn-primary btn-large center" %> |
|
| 7 |
+ <%= link_to "Login", new_user_session_path, :class => "btn btn-large" %> |
|
| 8 |
+ <%= link_to "Signup", new_user_registration_path, :class => "btn btn-primary btn-large" %> |
|
| 8 | 9 |
</div> |
| 9 | 10 |
<div class="span3"> |
| 10 | 11 |
<%= image_tag 'odin.jpg', :class => 'img-rounded', :title => "Wägner, Wilhelm. 1882. Nordisch-germanische Götter und Helden. Otto Spamer, Leipzig & Berlin. Page 7." %> |
@@ -0,0 +1,5 @@ |
||
| 1 |
+class AddKeepEventsForToAgents < ActiveRecord::Migration |
|
| 2 |
+ def change |
|
| 3 |
+ add_column :agents, :keep_events_for, :integer, :null => false, :default => 0 |
|
| 4 |
+ end |
|
| 5 |
+end |
@@ -11,7 +11,7 @@ |
||
| 11 | 11 |
# |
| 12 | 12 |
# It's strongly recommended to check this file into your version control system. |
| 13 | 13 |
|
| 14 |
-ActiveRecord::Schema.define(:version => 20131105063248) do |
|
| 14 |
+ActiveRecord::Schema.define(:version => 20131227000021) do |
|
| 15 | 15 |
|
| 16 | 16 |
create_table "agent_logs", :force => true do |t| |
| 17 | 17 |
t.integer "agent_id", :null => false |
@@ -33,10 +33,13 @@ ActiveRecord::Schema.define(:version => 20131105063248) do |
||
| 33 | 33 |
t.datetime "last_check_at" |
| 34 | 34 |
t.datetime "last_receive_at" |
| 35 | 35 |
t.integer "last_checked_event_id" |
| 36 |
- t.datetime "created_at", :null => false |
|
| 37 |
- t.datetime "updated_at", :null => false |
|
| 36 |
+ t.datetime "created_at", :null => false |
|
| 37 |
+ t.datetime "updated_at", :null => false |
|
| 38 | 38 |
t.text "memory", :limit => 2147483647 |
| 39 | 39 |
t.datetime "last_webhook_at" |
| 40 |
+ t.datetime "last_event_at" |
|
| 41 |
+ t.datetime "last_error_log_at" |
|
| 42 |
+ t.integer "keep_events_for", :default => 0, :null => false |
|
| 40 | 43 |
end |
| 41 | 44 |
|
| 42 | 45 |
add_index "agents", ["schedule"], :name => "index_agents_on_schedule" |
@@ -49,7 +49,7 @@ describe EventsController do |
||
| 49 | 49 |
}.should change { Event.count }.by(1)
|
| 50 | 50 |
Event.last.payload.should == events(:bob_website_agent_event).payload |
| 51 | 51 |
Event.last.agent.should == events(:bob_website_agent_event).agent |
| 52 |
- Event.last.created_at.should be_within(1).of(Time.now) |
|
| 52 |
+ Event.last.created_at.to_i.should be_within(2).of(Time.now.to_i) |
|
| 53 | 53 |
end |
| 54 | 54 |
|
| 55 | 55 |
it "can only re-emit Events for the current user" do |
@@ -1,6 +1,7 @@ |
||
| 1 | 1 |
jane_website_agent: |
| 2 | 2 |
type: Agents::WebsiteAgent |
| 3 | 3 |
user: jane |
| 4 |
+ events_count: 1 |
|
| 4 | 5 |
schedule: "5pm" |
| 5 | 6 |
name: "ZKCD" |
| 6 | 7 |
options: <%= {
|
@@ -16,6 +17,7 @@ jane_website_agent: |
||
| 16 | 17 |
bob_website_agent: |
| 17 | 18 |
type: Agents::WebsiteAgent |
| 18 | 19 |
user: bob |
| 20 |
+ events_count: 1 |
|
| 19 | 21 |
schedule: "midnight" |
| 20 | 22 |
name: "ZKCD" |
| 21 | 23 |
options: <%= {
|
@@ -33,6 +35,7 @@ bob_weather_agent: |
||
| 33 | 35 |
user: bob |
| 34 | 36 |
schedule: "midnight" |
| 35 | 37 |
name: "SF Weather" |
| 38 |
+ keep_events_for: 45 |
|
| 36 | 39 |
options: <%= {
|
| 37 | 40 |
:location => 94102, |
| 38 | 41 |
:lat => 37.779329, |
@@ -45,6 +48,7 @@ jane_weather_agent: |
||
| 45 | 48 |
user: jane |
| 46 | 49 |
schedule: "midnight" |
| 47 | 50 |
name: "SF Weather" |
| 51 |
+ keep_events_for: 30 |
|
| 48 | 52 |
options: <%= {
|
| 49 | 53 |
:location => 94103, |
| 50 | 54 |
:lat => 37.779329, |
@@ -249,7 +249,7 @@ describe Agent do |
||
| 249 | 249 |
agent.should have(0).errors_on(:base) |
| 250 | 250 |
end |
| 251 | 251 |
|
| 252 |
- it "symbolizes options before validating" do |
|
| 252 |
+ it "makes options symbol-indifferent before validating" do |
|
| 253 | 253 |
agent = Agents::SomethingSource.new(:name => "something") |
| 254 | 254 |
agent.user = users(:bob) |
| 255 | 255 |
agent.options["bad"] = true |
@@ -258,7 +258,7 @@ describe Agent do |
||
| 258 | 258 |
agent.should have(0).errors_on(:base) |
| 259 | 259 |
end |
| 260 | 260 |
|
| 261 |
- it "symbolizes memory before validating" do |
|
| 261 |
+ it "makes memory symbol-indifferent before validating" do |
|
| 262 | 262 |
agent = Agents::SomethingSource.new(:name => "something") |
| 263 | 263 |
agent.user = users(:bob) |
| 264 | 264 |
agent.memory["bad"] = 2 |
@@ -318,7 +318,85 @@ describe Agent do |
||
| 318 | 318 |
agent.user = users(:jane) |
| 319 | 319 |
agent.should have(0).errors_on(:sources) |
| 320 | 320 |
end |
| 321 |
+ |
|
| 322 |
+ it "validates keep_events_for" do |
|
| 323 |
+ agent = Agents::SomethingSource.new(:name => "something") |
|
| 324 |
+ agent.user = users(:bob) |
|
| 325 |
+ agent.should be_valid |
|
| 326 |
+ agent.keep_events_for = nil |
|
| 327 |
+ agent.should have(1).errors_on(:keep_events_for) |
|
| 328 |
+ agent.keep_events_for = 1000 |
|
| 329 |
+ agent.should have(1).errors_on(:keep_events_for) |
|
| 330 |
+ agent.keep_events_for = "" |
|
| 331 |
+ agent.should have(1).errors_on(:keep_events_for) |
|
| 332 |
+ agent.keep_events_for = 5 |
|
| 333 |
+ agent.should be_valid |
|
| 334 |
+ agent.keep_events_for = 0 |
|
| 335 |
+ agent.should be_valid |
|
| 336 |
+ agent.keep_events_for = 365 |
|
| 337 |
+ agent.should be_valid |
|
| 338 |
+ |
|
| 339 |
+ # Rails seems to call to_i on the input. This guards against future changes to that behavior. |
|
| 340 |
+ agent.keep_events_for = "drop table;" |
|
| 341 |
+ agent.keep_events_for.should == 0 |
|
| 342 |
+ end |
|
| 321 | 343 |
end |
| 344 |
+ |
|
| 345 |
+ describe "cleaning up now-expired events" do |
|
| 346 |
+ before do |
|
| 347 |
+ @agent = Agents::SomethingSource.new(:name => "something") |
|
| 348 |
+ @agent.keep_events_for = 5 |
|
| 349 |
+ @agent.user = users(:bob) |
|
| 350 |
+ @agent.save! |
|
| 351 |
+ @event = @agent.create_event :payload => { "hello" => "world" }
|
|
| 352 |
+ @event.expires_at.to_i.should be_within(2).of(5.days.from_now.to_i) |
|
| 353 |
+ end |
|
| 354 |
+ |
|
| 355 |
+ describe "when keep_events_for has not changed" do |
|
| 356 |
+ it "does nothing" do |
|
| 357 |
+ mock(@agent).update_event_expirations!.times(0) |
|
| 358 |
+ |
|
| 359 |
+ @agent.options[:foo] = "bar1" |
|
| 360 |
+ @agent.save! |
|
| 361 |
+ |
|
| 362 |
+ @agent.options[:foo] = "bar1" |
|
| 363 |
+ @agent.keep_events_for = 5 |
|
| 364 |
+ @agent.save! |
|
| 365 |
+ end |
|
| 366 |
+ end |
|
| 367 |
+ |
|
| 368 |
+ describe "when keep_events_for is changed" do |
|
| 369 |
+ it "updates events' expires_at" do |
|
| 370 |
+ lambda {
|
|
| 371 |
+ @agent.options[:foo] = "bar1" |
|
| 372 |
+ @agent.keep_events_for = 3 |
|
| 373 |
+ @agent.save! |
|
| 374 |
+ }.should change { @event.reload.expires_at }
|
|
| 375 |
+ @event.expires_at.to_i.should be_within(2).of(3.days.from_now.to_i) |
|
| 376 |
+ end |
|
| 377 |
+ |
|
| 378 |
+ it "updates events relative to their created_at" do |
|
| 379 |
+ @event.update_attribute :created_at, 2.days.ago |
|
| 380 |
+ @event.reload.created_at.to_i.should be_within(2).of(2.days.ago.to_i) |
|
| 381 |
+ |
|
| 382 |
+ lambda {
|
|
| 383 |
+ @agent.options[:foo] = "bar2" |
|
| 384 |
+ @agent.keep_events_for = 3 |
|
| 385 |
+ @agent.save! |
|
| 386 |
+ }.should change { @event.reload.expires_at }
|
|
| 387 |
+ @event.expires_at.to_i.should be_within(2).of(1.days.from_now.to_i) |
|
| 388 |
+ end |
|
| 389 |
+ |
|
| 390 |
+ it "nulls out expires_at when keep_events_for is set to 0" do |
|
| 391 |
+ lambda {
|
|
| 392 |
+ @agent.options[:foo] = "bar" |
|
| 393 |
+ @agent.keep_events_for = 0 |
|
| 394 |
+ @agent.save! |
|
| 395 |
+ }.should change { @event.reload.expires_at }.to(nil)
|
|
| 396 |
+ end |
|
| 397 |
+ end |
|
| 398 |
+ end |
|
| 399 |
+ |
|
| 322 | 400 |
end |
| 323 | 401 |
|
| 324 | 402 |
describe "recent_error_logs?" do |
@@ -371,4 +449,28 @@ describe Agent do |
||
| 371 | 449 |
end |
| 372 | 450 |
end |
| 373 | 451 |
end |
| 452 |
+ |
|
| 453 |
+ describe "#create_event" do |
|
| 454 |
+ describe "when the agent has keep_events_for set" do |
|
| 455 |
+ before do |
|
| 456 |
+ agents(:jane_weather_agent).keep_events_for.should > 0 |
|
| 457 |
+ end |
|
| 458 |
+ |
|
| 459 |
+ it "sets expires_at on created events" do |
|
| 460 |
+ event = agents(:jane_weather_agent).create_event :payload => { 'hi' => 'there' }
|
|
| 461 |
+ event.expires_at.to_i.should be_within(5).of(agents(:jane_weather_agent).keep_events_for.days.from_now.to_i) |
|
| 462 |
+ end |
|
| 463 |
+ end |
|
| 464 |
+ |
|
| 465 |
+ describe "when the agent does not have keep_events_for set" do |
|
| 466 |
+ before do |
|
| 467 |
+ agents(:jane_website_agent).keep_events_for.should == 0 |
|
| 468 |
+ end |
|
| 469 |
+ |
|
| 470 |
+ it "does not set expires_at on created events" do |
|
| 471 |
+ event = agents(:jane_website_agent).create_event :payload => { 'hi' => 'there' }
|
|
| 472 |
+ event.expires_at.should be_nil |
|
| 473 |
+ end |
|
| 474 |
+ end |
|
| 475 |
+ end |
|
| 374 | 476 |
end |
@@ -4,15 +4,15 @@ describe Agents::WebsiteAgent do |
||
| 4 | 4 |
before do |
| 5 | 5 |
stub_request(:any, /xkcd/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200)
|
| 6 | 6 |
@site = {
|
| 7 |
- :name => "XKCD", |
|
| 8 |
- :expected_update_period_in_days => 2, |
|
| 9 |
- :type => "html", |
|
| 10 |
- :url => "http://xkcd.com", |
|
| 11 |
- :mode => :on_change, |
|
| 12 |
- :extract => {
|
|
| 13 |
- :url => {:css => "#comic img", :attr => "src"},
|
|
| 14 |
- :title => {:css => "#comic img", :attr => "title"}
|
|
| 15 |
- } |
|
| 7 |
+ 'name' => "XKCD", |
|
| 8 |
+ 'expected_update_period_in_days' => 2, |
|
| 9 |
+ 'type' => "html", |
|
| 10 |
+ 'url' => "http://xkcd.com", |
|
| 11 |
+ 'mode' => 'on_change', |
|
| 12 |
+ 'extract' => {
|
|
| 13 |
+ 'url' => {'css' => "#comic img", 'attr' => "src"},
|
|
| 14 |
+ 'title' => {'css' => "#comic img", 'attr' => "title"}
|
|
| 15 |
+ } |
|
| 16 | 16 |
} |
| 17 | 17 |
@checker = Agents::WebsiteAgent.new(:name => "xkcd", :options => @site) |
| 18 | 18 |
@checker.user = users(:bob) |
@@ -27,7 +27,7 @@ describe Agents::WebsiteAgent do |
||
| 27 | 27 |
|
| 28 | 28 |
it "should always save events when in :all mode" do |
| 29 | 29 |
lambda {
|
| 30 |
- @site[:mode] = :all |
|
| 30 |
+ @site['mode'] = 'all' |
|
| 31 | 31 |
@checker.options = @site |
| 32 | 32 |
@checker.check |
| 33 | 33 |
@checker.check |
@@ -35,7 +35,7 @@ describe Agents::WebsiteAgent do |
||
| 35 | 35 |
end |
| 36 | 36 |
|
| 37 | 37 |
it "should log an error if the number of results for a set of extraction patterns differs" do |
| 38 |
- @site[:extract][:url][:css] = "div" |
|
| 38 |
+ @site['extract']['url']['css'] = "div" |
|
| 39 | 39 |
@checker.options = @site |
| 40 | 40 |
@checker.check |
| 41 | 41 |
@checker.logs.first.message.should =~ /Got an uneven number of matches/ |
@@ -68,20 +68,20 @@ describe Agents::WebsiteAgent do |
||
| 68 | 68 |
it "parses CSS" do |
| 69 | 69 |
@checker.check |
| 70 | 70 |
event = Event.last |
| 71 |
- event.payload[:url].should == "http://imgs.xkcd.com/comics/evolving.png" |
|
| 72 |
- event.payload[:title].should =~ /^Biologists play reverse/ |
|
| 71 |
+ event.payload['url'].should == "http://imgs.xkcd.com/comics/evolving.png" |
|
| 72 |
+ event.payload['title'].should =~ /^Biologists play reverse/ |
|
| 73 | 73 |
end |
| 74 | 74 |
|
| 75 | 75 |
it "should turn relative urls to absolute" do |
| 76 | 76 |
rel_site = {
|
| 77 |
- :name => "XKCD", |
|
| 78 |
- :expected_update_period_in_days => 2, |
|
| 79 |
- :type => "html", |
|
| 80 |
- :url => "http://xkcd.com", |
|
| 81 |
- :mode => :on_change, |
|
| 82 |
- :extract => {
|
|
| 83 |
- :url => {:css => "#topLeft a", :attr => "href"},
|
|
| 84 |
- :title => {:css => "#topLeft a", :text => "true"}
|
|
| 77 |
+ 'name' => "XKCD", |
|
| 78 |
+ 'expected_update_period_in_days' => 2, |
|
| 79 |
+ 'type' => "html", |
|
| 80 |
+ 'url' => "http://xkcd.com", |
|
| 81 |
+ 'mode' => :on_change, |
|
| 82 |
+ 'extract' => {
|
|
| 83 |
+ 'url' => {'css' => "#topLeft a", 'attr' => "href"},
|
|
| 84 |
+ 'title' => {'css' => "#topLeft a", 'text' => "true"}
|
|
| 85 | 85 |
} |
| 86 | 86 |
} |
| 87 | 87 |
rel = Agents::WebsiteAgent.new(:name => "xkcd", :options => rel_site) |
@@ -89,28 +89,28 @@ describe Agents::WebsiteAgent do |
||
| 89 | 89 |
rel.save! |
| 90 | 90 |
rel.check |
| 91 | 91 |
event = Event.last |
| 92 |
- event.payload[:url].should == "http://xkcd.com/about" |
|
| 92 |
+ event.payload['url'].should == "http://xkcd.com/about" |
|
| 93 | 93 |
end |
| 94 |
- |
|
| 94 |
+ |
|
| 95 | 95 |
describe "JSON" do |
| 96 | 96 |
it "works with paths" do |
| 97 | 97 |
json = {
|
| 98 |
- :response => {
|
|
| 99 |
- :version => 2, |
|
| 100 |
- :title => "hello!" |
|
| 101 |
- } |
|
| 98 |
+ 'response' => {
|
|
| 99 |
+ 'version' => 2, |
|
| 100 |
+ 'title' => "hello!" |
|
| 101 |
+ } |
|
| 102 | 102 |
} |
| 103 | 103 |
stub_request(:any, /json-site/).to_return(:body => json.to_json, :status => 200) |
| 104 | 104 |
site = {
|
| 105 |
- :name => "Some JSON Response", |
|
| 106 |
- :expected_update_period_in_days => 2, |
|
| 107 |
- :type => "json", |
|
| 108 |
- :url => "http://json-site.com", |
|
| 109 |
- :mode => :on_change, |
|
| 110 |
- :extract => {
|
|
| 111 |
- :version => { :path => "response.version" },
|
|
| 112 |
- :title => { :path => "response.title" }
|
|
| 113 |
- } |
|
| 105 |
+ 'name' => "Some JSON Response", |
|
| 106 |
+ 'expected_update_period_in_days' => 2, |
|
| 107 |
+ 'type' => "json", |
|
| 108 |
+ 'url' => "http://json-site.com", |
|
| 109 |
+ 'mode' => 'on_change', |
|
| 110 |
+ 'extract' => {
|
|
| 111 |
+ 'version' => {'path' => "response.version"},
|
|
| 112 |
+ 'title' => {'path' => "response.title"}
|
|
| 113 |
+ } |
|
| 114 | 114 |
} |
| 115 | 115 |
checker = Agents::WebsiteAgent.new(:name => "Weather Site", :options => site) |
| 116 | 116 |
checker.user = users(:bob) |
@@ -118,30 +118,30 @@ describe Agents::WebsiteAgent do |
||
| 118 | 118 |
|
| 119 | 119 |
checker.check |
| 120 | 120 |
event = Event.last |
| 121 |
- event.payload[:version].should == 2 |
|
| 122 |
- event.payload[:title].should == "hello!" |
|
| 121 |
+ event.payload['version'].should == 2 |
|
| 122 |
+ event.payload['title'].should == "hello!" |
|
| 123 | 123 |
end |
| 124 | 124 |
|
| 125 | 125 |
it "can handle arrays" do |
| 126 | 126 |
json = {
|
| 127 |
- :response => {
|
|
| 128 |
- :data => [ |
|
| 129 |
- { :title => "first", :version => 2 },
|
|
| 130 |
- { :title => "second", :version => 2.5 }
|
|
| 131 |
- ] |
|
| 132 |
- } |
|
| 127 |
+ 'response' => {
|
|
| 128 |
+ 'data' => [ |
|
| 129 |
+ {'title' => "first", 'version' => 2},
|
|
| 130 |
+ {'title' => "second", 'version' => 2.5}
|
|
| 131 |
+ ] |
|
| 132 |
+ } |
|
| 133 | 133 |
} |
| 134 | 134 |
stub_request(:any, /json-site/).to_return(:body => json.to_json, :status => 200) |
| 135 | 135 |
site = {
|
| 136 |
- :name => "Some JSON Response", |
|
| 137 |
- :expected_update_period_in_days => 2, |
|
| 138 |
- :type => "json", |
|
| 139 |
- :url => "http://json-site.com", |
|
| 140 |
- :mode => :on_change, |
|
| 141 |
- :extract => {
|
|
| 142 |
- :title => { :path => "response.data[*].title" },
|
|
| 143 |
- :version => { :path => "response.data[*].version" }
|
|
| 144 |
- } |
|
| 136 |
+ 'name' => "Some JSON Response", |
|
| 137 |
+ 'expected_update_period_in_days' => 2, |
|
| 138 |
+ 'type' => "json", |
|
| 139 |
+ 'url' => "http://json-site.com", |
|
| 140 |
+ 'mode' => 'on_change', |
|
| 141 |
+ 'extract' => {
|
|
| 142 |
+ :title => {'path' => "response.data[*].title"},
|
|
| 143 |
+ :version => {'path' => "response.data[*].version"}
|
|
| 144 |
+ } |
|
| 145 | 145 |
} |
| 146 | 146 |
checker = Agents::WebsiteAgent.new(:name => "Weather Site", :options => site) |
| 147 | 147 |
checker.user = users(:bob) |
@@ -152,28 +152,28 @@ describe Agents::WebsiteAgent do |
||
| 152 | 152 |
}.should change { Event.count }.by(2)
|
| 153 | 153 |
|
| 154 | 154 |
event = Event.all[-1] |
| 155 |
- event.payload[:version].should == 2.5 |
|
| 156 |
- event.payload[:title].should == "second" |
|
| 155 |
+ event.payload['version'].should == 2.5 |
|
| 156 |
+ event.payload['title'].should == "second" |
|
| 157 | 157 |
|
| 158 | 158 |
event = Event.all[-2] |
| 159 |
- event.payload[:version].should == 2 |
|
| 160 |
- event.payload[:title].should == "first" |
|
| 159 |
+ event.payload['version'].should == 2 |
|
| 160 |
+ event.payload['title'].should == "first" |
|
| 161 | 161 |
end |
| 162 | 162 |
|
| 163 | 163 |
it "stores the whole object if :extract is not specified" do |
| 164 | 164 |
json = {
|
| 165 |
- :response => {
|
|
| 166 |
- :version => 2, |
|
| 167 |
- :title => "hello!" |
|
| 168 |
- } |
|
| 165 |
+ 'response' => {
|
|
| 166 |
+ 'version' => 2, |
|
| 167 |
+ 'title' => "hello!" |
|
| 168 |
+ } |
|
| 169 | 169 |
} |
| 170 | 170 |
stub_request(:any, /json-site/).to_return(:body => json.to_json, :status => 200) |
| 171 | 171 |
site = {
|
| 172 |
- :name => "Some JSON Response", |
|
| 173 |
- :expected_update_period_in_days => 2, |
|
| 174 |
- :type => "json", |
|
| 175 |
- :url => "http://json-site.com", |
|
| 176 |
- :mode => :on_change |
|
| 172 |
+ 'name' => "Some JSON Response", |
|
| 173 |
+ 'expected_update_period_in_days' => 2, |
|
| 174 |
+ 'type' => "json", |
|
| 175 |
+ 'url' => "http://json-site.com", |
|
| 176 |
+ 'mode' => 'on_change' |
|
| 177 | 177 |
} |
| 178 | 178 |
checker = Agents::WebsiteAgent.new(:name => "Weather Site", :options => site) |
| 179 | 179 |
checker.user = users(:bob) |
@@ -181,8 +181,8 @@ describe Agents::WebsiteAgent do |
||
| 181 | 181 |
|
| 182 | 182 |
checker.check |
| 183 | 183 |
event = Event.last |
| 184 |
- event.payload[:response][:version].should == 2 |
|
| 185 |
- event.payload[:response][:title].should == "hello!" |
|
| 184 |
+ event.payload['response']['version'].should == 2 |
|
| 185 |
+ event.payload['response']['title'].should == "hello!" |
|
| 186 | 186 |
end |
| 187 | 187 |
end |
| 188 | 188 |
end |
@@ -13,25 +13,50 @@ describe Event do |
||
| 13 | 13 |
Event.last.agent.should == events(:bob_website_agent_event).agent |
| 14 | 14 |
Event.last.lat.should == 2 |
| 15 | 15 |
Event.last.lng.should == 3 |
| 16 |
- Event.last.created_at.should be_within(1).of(Time.now) |
|
| 16 |
+ Event.last.created_at.to_i.should be_within(2).of(Time.now.to_i) |
|
| 17 | 17 |
end |
| 18 | 18 |
end |
| 19 | 19 |
|
| 20 | 20 |
describe ".cleanup_expired!" do |
| 21 |
- it "removes any Events whose expired_at date is non-null and in the past" do |
|
| 22 |
- event = agents(:jane_weather_agent).create_event :expires_at => 2.hours.from_now |
|
| 21 |
+ it "removes any Events whose expired_at date is non-null and in the past, updating Agent counter caches" do |
|
| 22 |
+ half_hour_event = agents(:jane_weather_agent).create_event :expires_at => 20.minutes.from_now |
|
| 23 |
+ one_hour_event = agents(:bob_weather_agent).create_event :expires_at => 1.hours.from_now |
|
| 24 |
+ two_hour_event = agents(:jane_weather_agent).create_event :expires_at => 2.hours.from_now |
|
| 25 |
+ three_hour_event = agents(:jane_weather_agent).create_event :expires_at => 3.hours.from_now |
|
| 26 |
+ non_expiring_event = agents(:bob_weather_agent).create_event({})
|
|
| 27 |
+ |
|
| 28 |
+ initial_bob_count = agents(:bob_weather_agent).reload.events_count |
|
| 29 |
+ initial_jane_count = agents(:jane_weather_agent).reload.events_count |
|
| 23 | 30 |
|
| 24 | 31 |
current_time = Time.now |
| 25 | 32 |
stub(Time).now { current_time }
|
| 26 | 33 |
|
| 27 | 34 |
Event.cleanup_expired! |
| 28 |
- Event.find_by_id(event.id).should_not be_nil |
|
| 29 |
- current_time = 119.minutes.from_now |
|
| 35 |
+ Event.find_by_id(half_hour_event.id).should_not be_nil |
|
| 36 |
+ Event.find_by_id(one_hour_event.id).should_not be_nil |
|
| 37 |
+ Event.find_by_id(two_hour_event.id).should_not be_nil |
|
| 38 |
+ Event.find_by_id(three_hour_event.id).should_not be_nil |
|
| 39 |
+ Event.find_by_id(non_expiring_event.id).should_not be_nil |
|
| 40 |
+ agents(:bob_weather_agent).reload.events_count.should == initial_bob_count |
|
| 41 |
+ agents(:jane_weather_agent).reload.events_count.should == initial_jane_count |
|
| 42 |
+ |
|
| 43 |
+ current_time = 119.minutes.from_now # move almost 2 hours into the future |
|
| 30 | 44 |
Event.cleanup_expired! |
| 31 |
- Event.find_by_id(event.id).should_not be_nil |
|
| 32 |
- current_time = 2.minutes.from_now |
|
| 45 |
+ Event.find_by_id(half_hour_event.id).should be_nil |
|
| 46 |
+ Event.find_by_id(one_hour_event.id).should be_nil |
|
| 47 |
+ Event.find_by_id(two_hour_event.id).should_not be_nil |
|
| 48 |
+ Event.find_by_id(three_hour_event.id).should_not be_nil |
|
| 49 |
+ Event.find_by_id(non_expiring_event.id).should_not be_nil |
|
| 50 |
+ agents(:bob_weather_agent).reload.events_count.should == initial_bob_count - 1 |
|
| 51 |
+ agents(:jane_weather_agent).reload.events_count.should == initial_jane_count - 1 |
|
| 52 |
+ |
|
| 53 |
+ current_time = 2.minutes.from_now # move 2 minutes further into the future |
|
| 33 | 54 |
Event.cleanup_expired! |
| 34 |
- Event.find_by_id(event.id).should be_nil |
|
| 55 |
+ Event.find_by_id(two_hour_event.id).should be_nil |
|
| 56 |
+ Event.find_by_id(three_hour_event.id).should_not be_nil |
|
| 57 |
+ Event.find_by_id(non_expiring_event.id).should_not be_nil |
|
| 58 |
+ agents(:bob_weather_agent).reload.events_count.should == initial_bob_count - 1 |
|
| 59 |
+ agents(:jane_weather_agent).reload.events_count.should == initial_jane_count - 2 |
|
| 35 | 60 |
end |
| 36 | 61 |
|
| 37 | 62 |
it "doesn't touch Events with no expired_at" do |